﻿using System;
using System.Collections.Generic;
using System.Linq;

namespace SpecUnit
{
    public class Specification
    {
        #region Fluent Interface

        public interface IScenarioContext
        {
            IExpressionFollowedBy<IExtendedContextOrTrigger> Given(string format, params object[] args);
            IExpressionFollowedBy<IExtendedTriggerOrOutcome> When(string format, params object[] args);
        }

        public interface IExtendedContextOrTrigger
        {
            IExpressionFollowedBy<IExtendedContextOrTrigger> And(string format, params object[] args);
            IExpressionFollowedBy<IExtendedTriggerOrOutcome> When(string format, params object[] args);
        }

        public interface IExtendedTriggerOrOutcome
        {
            IExpressionFollowedBy<IExtendedTriggerOrOutcome> And(string format, params object[] args);
            IExpressionFollowedBy<IExtendedOutcomeOrCompile> Then(string format, params object[] args);
        }

        public interface IExtendedOutcomeOrCompile
        {
            IExpressionFollowedBy<IExtendedOutcomeOrCompile> And(string format, params object[] args);
            IScript AsScript { get; }
        }

        public interface IExpressionFollowedBy<out TClause>
        {
            TClause this[Action action] { get; }
        }

        #endregion

        public interface IStep
        {
            bool IsRequired { get; }
            string Description { get; }

            void Execute();
        }

        public interface IScript
        {
            string Description { get; }
            IEnumerable<IStep> Actions { get; }
        }

        public void IsSatisfiedBy(IScript script)
        {
            Runner.Verify(script);
        }

        public static IScenarioContext Scenario(string description)
        {
            return new ScenarioContext(description);
        }

        public static void NoAction()
        {
        }

        public static void Pending()
        {
            throw new NotImplementedException();
        }

        #region Runner

        public static class Runner
        {
            static Runner()
            {
                Output = Console.WriteLine;
                Verify = DefaultVerification;
            }

            public static Action<string> Output { get; set; }

            public static Action<IScript> Verify { get; set; }

            public static void DefaultVerification(IScript script)
            {
                OutputScenario(script);

                var errorCount = 0;

                foreach (var step in script.Actions)
                {
                    try
                    {
                        step.Execute();
                    }
                    catch (Exception exception)
                    {
                        if (Output != null) Output(String.Format("Exception occurred when executing '{0}' : {1}", step.Description, exception.Message));
                        if (step.IsRequired) throw;

                        ++errorCount;
                    }
                }

                if (errorCount > 0) throw new Exception(String.Format("Scenario failed with {0} errors", errorCount));
            }

            public static void OutputScenario(IScript script)
            {
                if (Output == null || script == null || !script.Actions.Any()) return;

                Output("Scenario: " + script.Description);

                foreach (var step in script.Actions)
                {
                    Output("  " + step.Description);
                }

                Output(Environment.NewLine);
            }
        }

        #endregion

        #region Vocabulary

        public static class Vocabulary
        {
            public static string Given = "Given";
            public static string When  = " When";
            public static string Then  = " Then";
            public static string And   = "  And";
        }

        #endregion

        #region Implementation

        private sealed class Step : IStep
        {
            public bool IsRequired { get; set; }
            public string Description { get; set; }
// ReSharper disable MemberCanBePrivate.Local
            public Action Action { get; set; }
// ReSharper restore MemberCanBePrivate.Local

            public void Execute()
            {
                Action();
            }
        }

        private sealed class Script : IScript
        {
            public Script(string description, IEnumerable<Step> allSteps)
            {
                Description = description;
                Actions = allSteps;
            }

            public string Description { get; private set; }

            public IEnumerable<IStep> Actions { get; private set; }
        }

        private abstract class Clause
        {
            private Clause PreviousClause { get; set; }

            public Step Descriptor { get; private set; }

            protected IEnumerable<Clause> Precedents
            {
                get
                {
                    var behavior = PreviousClause;

                    while (behavior != null)
                    {
                        yield return behavior;
                        behavior = behavior.PreviousClause;
                    }
                }
            }

            protected IExpressionFollowedBy<TClause> SetupDescriptorFollowedBy<TClause>(string prefix, string format, object[] args)
                where TClause : Clause, new()
            {
                var nextClause = StepDescriptorFollowedBy<TClause>(prefix, format, args);

                Descriptor.IsRequired = true;
                return nextClause;
            }

            protected IExpressionFollowedBy<TClause> StepDescriptorFollowedBy<TClause>(string prefix, string format, object[] args)
                where TClause : Clause, new()
            {
                Descriptor = new Step { Description = prefix + " " + String.Format(format, args) };
                return new LinkTo<TClause>(this);
            }

            // Used as a psuedo l-value to support building a linked list of clauses.
            private sealed class LinkTo<TClause> : IExpressionFollowedBy<TClause> where TClause : Clause, new()
            {
                private readonly Clause _previousClause;

                public LinkTo(Clause previousClause)
                {
                    Guard.Against<ArgumentNullException>(previousClause == null, "previousClause cannot be null");
                    // ReSharper disable PossibleNullReferenceException
                    Guard.Against<ArgumentNullException>(previousClause.Descriptor == null, "Descriptor not specified for previousClause");
                    // ReSharper restore PossibleNullReferenceException

                    _previousClause = previousClause;
                }

                public TClause this[Action action]
                {
                    get
                    {
                        Guard.Against<ArgumentNullException>(action == null, "Action cannot be null");
                        _previousClause.Descriptor.Action = action;

                        return new TClause { PreviousClause = _previousClause };
                    }
                }
            }
        }

        #region Clauses

        private sealed class ScenarioContext : Clause, IScenarioContext
        {
            public ScenarioContext(string description)
            {
                Guard.Against<ArgumentException>(String.IsNullOrWhiteSpace(description), "Requirement description is required");
                Description = description;
            }

            public string Description
            {
                get;
                private set;
            }

            public IExpressionFollowedBy<IExtendedContextOrTrigger> Given(string format, params object[] args)
            {
                return SetupDescriptorFollowedBy<ExtendedContextOrTrigger>(Vocabulary.Given, format, args);
            }

            public IExpressionFollowedBy<IExtendedTriggerOrOutcome> When(string format, params object[] args)
            {
                return SetupDescriptorFollowedBy<ExtendedTriggerOrOutcome>(Vocabulary.When, format, args);
            }
        }

        private sealed class ExtendedContextOrTrigger : Clause, IExtendedContextOrTrigger
        {
            public IExpressionFollowedBy<IExtendedTriggerOrOutcome> When(string format, params object[] args)
            {
                return SetupDescriptorFollowedBy<ExtendedTriggerOrOutcome>(Vocabulary.When, format, args);
            }

            public IExpressionFollowedBy<IExtendedContextOrTrigger> And(string format, params object[] args)
            {
                return SetupDescriptorFollowedBy<ExtendedContextOrTrigger>(Vocabulary.And, format, args);
            }
        }

        private sealed class ExtendedTriggerOrOutcome : Clause, IExtendedTriggerOrOutcome
        {
            public IExpressionFollowedBy<IExtendedOutcomeOrCompile> Then(string format, params object[] args)
            {
                return StepDescriptorFollowedBy<ExtendedOutcomeOrCompile>(Vocabulary.Then, format, args);
            }

            public IExpressionFollowedBy<IExtendedTriggerOrOutcome> And(string format, params object[] args)
            {
                return SetupDescriptorFollowedBy<ExtendedTriggerOrOutcome>(Vocabulary.And, format, args);
            }
        }

        private sealed class ExtendedOutcomeOrCompile : Clause, IExtendedOutcomeOrCompile
        {
            public IExpressionFollowedBy<IExtendedOutcomeOrCompile> And(string format, params object[] args)
            {
                return StepDescriptorFollowedBy<ExtendedOutcomeOrCompile>(Vocabulary.And, format, args);
            }

            public IScript AsScript
            {
                get
                {
                    var allClauses = Precedents.Reverse();

                    return new Script(
                        allClauses.OfType<ScenarioContext>().First().Description,
                        allClauses.Select(clause => clause.Descriptor).ToArray());
                }
            }
        }

        #endregion Clauses

        #endregion Implementation
    }
}
